/**
 * Copyright (c) 2010-2011 Pierre Labatut
 *
 * This file is part of Space-Chronicles project.
 *
 * Space-Chronicles is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/space-chronicles/>.
 */
package space.app.meshview;

import java.net.URL;
import space.app.meshview.state.MeshCamera;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.media.opengl.DebugGL3bc;
import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GL3;
import javax.media.opengl.GL3bc;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLRunnable;
import jinngine.math.Matrix4;
import jinngine.math.Quaternion;
import jinngine.math.Vector3;
import space.game.ShaderProgram;

import space.game.WldMaterial;
import space.game.world.Billboard;
import space.game.world.Trail;
import space.game.world.WldInert;
import space.game.world.WldLightCSM;
import space.game.world.WldLightCSMSlice;
import space.game.world.WldScene;
import space.gl.Program;
import space.gl.RenderContext;
import space.gl.RenderContext.Pass;
import space.glsl.GLSLDirectionalLight;
import space.glsl.GLSLMaterial;
import space.math.Vector3f;
import space.model.Actor;
import space.model.Drawable;
import space.model.Material;
import space.model.MdlEmpty;
import space.model.MdlLibrary;
import space.model.MdlMeshArray;
import space.model.MdlObject;
import space.model.RenderingCamera;
import space.stream.StructureInput;

/**
 *
 * @author Pierre
 */
class MeshEventListner implements GLEventListener, WldScene {

    public static boolean mesh_only = true;

    private GL3bc buildGL(GLAutoDrawable drawable) {
        return new DebugGL3bc((GL3bc) drawable.getGL());
    }
    private final Queue<GLRunnable> operations =
            new ConcurrentLinkedQueue<GLRunnable>();
    final Quaternion lightDirection = new Quaternion(1, 0, 0, 0);
    final MeshCamera camera = new MeshCamera();
    private WldLightCSM sunlight;
    boolean showTangentSpace = false;
    boolean showFBO = false;
    Program zClampPS;
    Program normalPS;
    Program additivePS;
    Drawable drawn;
    MdlMeshArray ddrawn;
    final Trail trail = new Trail(new Vector3());
    Program trailProgram;

    {
        for (float t = 0; t < Math.PI * 2.; t += Math.PI / 32.) {
            final double s = 128.;
            final double x = (2. + Math.cos(3. * t)) * Math.cos(2. * t);
            final double y = (2. + Math.cos(3. * t)) * Math.sin(2. * t);
            final double z = Math.sin(3. * t);
            trail.addTrailPoint(Matrix4.translation(x * s, y * s, z * s));
        }
    }
    final GLSLDirectionalLight light = new GLSLDirectionalLight(
            "light",
            new Vector3(.0f, -.5f, .5f),
            new Vector3f(.4f, .4f, .5f),
            new Vector3f(.95f, .95f, .95f));
    final GLSLMaterial material = new GLSLMaterial(
            "material",
            .7f, new Vector3f(.2f, .2f, .2f));

    public void useShader(final ShaderProgram sp) {
        if (sp == null) {
            throw new IllegalArgumentException();
        }
        operations.add(new GLRunnable() {

            public void run(GLAutoDrawable drawable) {
                final GL3bc gl = buildGL(drawable);
                normalPS = sp.link(gl);
            }
        });
    }

    private Map<String, WldMaterial> loadMaterial(final GL3bc gl, final Map<String, Material> matLib, final URL url) {
        final Map<String, WldMaterial> wldmap = new HashMap<String, WldMaterial>();
        for (Entry<String, Material> e : matLib.entrySet()) {
            try {
                wldmap.put(e.getKey(),
                        new WldMaterial(gl, e.getValue(), url));
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
        return wldmap;
    }

    public void init(GLAutoDrawable drawable) {

        final GL3bc gl = buildGL(drawable);
        normalPS = ShaderProgram.DIFFUSE_MAP.link(gl);
        zClampPS = ShaderProgram.Z_CLAMP.link(gl);
        additivePS = ShaderProgram.ENGINE_GLOW.link(gl);
        // Enable Zbuffer
        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glDepthFunc(GL.GL_LEQUAL);

        // Enable backface culling
        gl.glEnable(GL.GL_CULL_FACE);

        sunlight = new WldLightCSM(gl);
        trailProgram = ShaderProgram.TRAIL_SHADE.link(gl);

        drawn = new Billboard();
    }

    public void dispose(GLAutoDrawable drawable) {
    }

    public void display(GLAutoDrawable drawable) {

        {// Run queued operations
            do {
                final GLRunnable op = operations.poll();
                if (op == null) {
                    break;
                }
                op.run(drawable);
            } while (true);
        }

        final Matrix4 lightTransformation = lightDirection.toMatrix4(new Matrix4());
        light.setDirection(lightTransformation.multiply(new Vector3(0., 0., -1.)));
        // Common usefull information
        final GL3bc gl = buildGL(drawable);
        final int viewportWidth = drawable.getWidth();
        final int viewportHeight = drawable.getHeight();
        final double aspect = viewportWidth / viewportHeight;
        final RenderingCamera renderingCamera = new RenderingCamera(camera);

        {   // Render Shadowmap
            final RenderContext rc = new RenderContext(gl, zClampPS, light, Matrix4.identity(),
                    Matrix4.identity(), Matrix4.identity(), Pass.SHADOW);
            sunlight.renderShadowMap(rc, lightTransformation, renderingCamera, aspect, this);
        }

        {  // Setup viewport
            gl.glViewport(0, 0, viewportWidth, viewportHeight);
            gl.glScissor(0, 0, viewportWidth, viewportHeight);
            gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT);
        }

        // Setup viewpoint
        final Matrix4 viewProjection = renderingCamera.perspective(
                viewportWidth, viewportHeight);
        final Matrix4 worldView = camera.viewpoint();
        final RenderContext rc = new RenderContext(gl, normalPS, light, viewProjection, worldView,
                Matrix4.identity(), Pass.SHADE);

        rc.applyLegacyMatrix();
        if (showTangentSpace && ddrawn != null) {// Display the Tangent space basis

            gl.glColor3d(1., 1., 1.);
            gl.glActiveTexture(GL2.GL_TEXTURE0);
            gl.glDisable(GL2.GL_TEXTURE_2D);
            gl.glActiveTexture(GL2.GL_TEXTURE1);
            gl.glDisable(GL2.GL_TEXTURE_2D);
            gl.glActiveTexture(GL2.GL_TEXTURE2);
            gl.glDisable(GL2.GL_TEXTURE_2D);
            ddrawn.draw(rc.create());
        }

        light.draw(gl);

        /**
         * When there is something to draw
         */
        normalPS.startUsingProgram(gl);
        rc.applyUniform1i("diffuseMap", 0);
        rc.applyUniform1i("normalMap", 1);
        material.applyUniform(rc);


        ////////////
        sunlight.draw(gl);
        {
            int paramIdx = gl.glGetUniformLocation(normalPS.programHandle,
                    "shadowMapMatrix");
            final Matrix4 lightMatrices[] = sunlight.getTextureCoordMatrix();
            final float m[] = new float[4 * 4 * 4];
            lightMatrices[0].toFloatArray(m, 0);
            lightMatrices[1].toFloatArray(m, 16);
            lightMatrices[2].toFloatArray(m, 32);
            lightMatrices[3].toFloatArray(m, 48);
            gl.glUniformMatrix4fv(paramIdx, 4, false, m, 0);
        }

        {
            int paramIdx = gl.glGetUniformLocation(normalPS.programHandle,
                    "splitDistance");
            WldLightCSMSlice[] slices = sunlight.getSlices();
            gl.glUniform4f(paramIdx, (float) slices[0].getNear(),
                    (float) slices[1].getNear(),
                    (float) slices[2].getNear(),
                    (float) slices[3].getNear());
        }

        gl.glActiveTexture(GL2.GL_TEXTURE2);
        gl.glEnable(GL2.GL_TEXTURE_2D);
        sunlight.bindTexture(gl);
        int loc2 = gl.glGetUniformLocation(normalPS.programHandle, "shadowMap");
        gl.glUniform1i(loc2, 2);

        ///////////
        draw(rc);
        rc.stopUsingProgram();


        final RenderContext additiveRC = new RenderContext(gl, additivePS, light, viewProjection,
                worldView, Matrix4.identity(), Pass.ADDITIVE);

        gl.glBlendFunc(GL3.GL_ONE, GL3.GL_ONE);
        gl.glEnable(GL.GL_BLEND);
        gl.glDepthMask(false);
        gl.glDisable(GL.GL_CULL_FACE);

        additiveRC.startUsingProgram();
        draw(additiveRC);
        additiveRC.stopUsingProgram();

        gl.glDepthMask(true);
        gl.glDisable(GL.GL_BLEND);



        gl.glActiveTexture(GL2.GL_TEXTURE0);
        gl.glDisable(GL2.GL_TEXTURE_2D);
        gl.glActiveTexture(GL2.GL_TEXTURE1);
        gl.glDisable(GL2.GL_TEXTURE_2D);
        gl.glActiveTexture(GL2.GL_TEXTURE2);
        gl.glDisable(GL2.GL_TEXTURE_2D);
        {
            gl.glMatrixMode(GL2.GL_MODELVIEW);
            final Matrix4 projection = camera.viewpoint();
            projection.a14 = 0.;
            projection.a24 = 0.;
            projection.a34 = -camera.currentDistance;
            gl.glLoadMatrixd(projection.toArray(), 0);
        }

        MdlEmpty.drawBasis(gl);
        camera.draw(gl);
        if (showFBO) {
            sunlight.debugFBO(gl);
        }
    }

    /**
     * Set the openGL settings
     * @param drawable no null surface that supports OpenGL 3 profile
     * @param x
     * @param y
     * @param width positive number of pixel on horizontal direction
     * @param height positive number of pixel on vertical direction
     */
    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        if (drawable == null) {
            throw new IllegalArgumentException();
        }
        if (width <= 0) {
            throw new IllegalArgumentException();
        }
        if (height <= 0) {
            throw new IllegalArgumentException();
        }
        final GL gl = buildGL(drawable);
        gl.glViewport(0, 0, width, height);
        gl.glScissor(0, 0, width, height);
    }

    MeshCamera mouseListener() {
        return camera;
    }

    void load(final URL url) {
        operations.add(new GLRunnable() {

            public void run(GLAutoDrawable drawable) {
                final GL3bc gl = buildGL(drawable);
                /**
                 * Load something
                 */
                final MdlLibrary lib = StructureInput.readObject(url, MdlLibrary.class);
                final MdlObject mesh = lib.getRoots().get(0);

                WldInert inert;
                drawn = inert = new WldInert(mesh, lib.getGroupLib(),
                        loadMaterial(gl, lib.getMaterialLib(), url), "Pegasus");
                ddrawn = inert.leak();


                drawn.optimize(gl);
                drawn.optimize(gl);

            }
        });
    }

    void setFBOVisible(final boolean selected) {
        operations.add(new GLRunnable() {

            public void run(GLAutoDrawable drawable) {
                showFBO = selected;
            }
        });
    }

    void setTBNVisible(final boolean selected) {
        operations.add(new GLRunnable() {

            public void run(GLAutoDrawable drawable) {
                showTangentSpace = selected;
            }
        });
    }

    public void addActor(Actor actor) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void draw(RenderContext rc) {
        if (drawn != null) {
            drawn.draw(rc.create());
        } else {
            //  trailProgram.startUsingProgram(rc.gl());
            //  rc.stopUsingProgram();
            //  trail.draw(rc, trailProgram);
            //  trailProgram.stopUsingProgram(rc.gl());
        }
    }
}
